/**
 * 
 */
package nl.ru.rd.facedetection.nnbfd.neuralnetwork;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;

import nl.ru.rd.facedetection.nnbfd.Matrix;

/**
 * A generic Neural Network
 * 
 * @author Wouter Geraedts (s0814857 - wgeraedts) PGP 66AA5935
 */
public class NeuralNetwork implements Serializable
{
	private static final long serialVersionUID = 2490711951840658616L;

	private ArrayList<Input> inputs = new ArrayList<Input>();
	private ArrayList<Layer> layers = new ArrayList<Layer>();

	private Layer outputLayer;

	/**
	 * Calculates the output of the neural network.
	 * 
	 * @param values
	 *            A single layered array with values between -1 and 1, having the same number of values as this network as inputs.
	 * @return An array of values between -1 and 1 corresponding to the output of the output Layer of this neural network.
	 */
	public double[] calculate(double[] values)
	{
		if(this.inputs.size() != values.length)
		{
			System.out.println(this.inputs.size() + " != " + values.length);
			throw new ArrayIndexOutOfBoundsException();
		}

		for(int i = 0; i < values.length; i++)
			this.inputs.get(i).setValue(values[i]);

		for(Layer layer : this.layers)
			layer.update();

		double[] result = new double[this.outputLayer.getSize()];
		for(int i = 0; i < result.length; i++)
			result[i] = this.outputLayer.getActivation(i);

		return result;
	}

	/**
	 * Learn a specific set of corresponding inputs and outputs, with a specific learningRate.
	 * 
	 * Calculates the output of the neural network by mapping a Matrix to a single layered array.
	 * 
	 * @param inputs
	 *            The Matrix of inputs to learn. The Matrix will be mapped to a single layered array (vector). The number of elements must be equals to the
	 *            number of registered Inputs of this Network.
	 * @param targets
	 *            The set of outputs to learn. The number of elements must be equals to the number of Nodes in the registered OutputLayer.
	 * @param learningRate
	 *            The maximum amount by which weights in this Network may be adjusted. (multiplier)
	 */
	public void learn(Matrix inputs, double[] targets, double learningRate)
	{
		this.learn(this.mapToDoubleArray(inputs), targets, learningRate);
	}

	/**
	 * Learn a specific set of corresponding inputs and outputs, with a specific learningRate.
	 * 
	 * @param inputs
	 *            The set of inputs to learn. The number of elements must be equals to the number of registered Inputs of this Network.
	 * @param targets
	 *            The set of outputs to learn. The number of elements must be equals to the number of Nodes in the registered OutputLayer.
	 * @param learningRate
	 *            The maximum amount by which weights in this Network may be adjusted. (multiplier)
	 */
	public void learn(double[] inputs, double[] targets, double learningRate)
	{
		this.calculate(inputs);

		((Backpropagatable) this.outputLayer).updateDeltas(targets);

		for(int i = this.layers.size() - 1; i >= 0; i--)
		{
			Layer layer = this.layers.get(i);
			if(Backpropagatable.class.isInstance(layer))
			{
				Backpropagatable bplayer = (Backpropagatable) layer;
				if(bplayer != this.outputLayer)
					bplayer.updateDeltas();
			}
		}

		for(int i = this.layers.size() - 1; i >= 0; i--)
		{
			Layer layer = this.layers.get(i);
			if(Backpropagatable.class.isInstance(layer))
				((Backpropagatable) layer).updateWeights(learningRate);
		}
	}

	/**
	 * Calculates the output of the neural network by mapping a Matrix to a single layered array.
	 * 
	 * @param values
	 *            A matrix with values between 0 and 255.
	 * @return An array of values between -1 and 1 corresponding to the output of the output Layer of this neural network.
	 */
	public double[] calculate(Matrix values)
	{
		return this.calculate(this.mapToDoubleArray(values));
	}

	/**
	 * Writes this Neural Network to a file.
	 * 
	 * @param fileName
	 *            The file to write the object to.
	 * @throws SecurityException
	 *             In case the file may not be written, or the object may not be serialized.
	 * @throws IOException
	 *             In case an error occurred while writing the file.
	 */
	public void toFile(String fileName) throws SecurityException, IOException
	{
		FileOutputStream file = new FileOutputStream(fileName);
		ObjectOutputStream objectStream = new ObjectOutputStream(file);
		objectStream.writeObject(this);
		objectStream.close();
	}

	/**
	 * Reads a Neural Network from a file and returns it.
	 * 
	 * @param fileName
	 *            The file in which the Neural Network was stored.
	 * @return The Neural Network stored by the file.
	 */
	public static NeuralNetwork readFromFile(String fileName)
	{
		NeuralNetwork network = null;

		try
		{
			FileInputStream file = new FileInputStream(fileName);
			ObjectInputStream objectStream = new ObjectInputStream(file);
			network = (NeuralNetwork) objectStream.readObject();
			objectStream.close();
		} catch (ClassNotFoundException e)
		{ // Return null
		} catch (ClassCastException e)
		{ // Return null
		} catch (IOException e)
		{ // Return null
		}

		return network;
	}

	/**
	 * Maps an array with shorts and values between 0 and 255 to a single layered array with values between -1 and 1.
	 * 
	 * @param matrix
	 *            The matrix to be mapped with values between 0 and 255.
	 * @return A single layered array with values between -1 and 1.
	 */
	protected double[] mapToDoubleArray(Matrix matrix)
	{
		int width = matrix.getWidth();
		int height = matrix.getHeight();
		double[] result = new double[width * height];

		for(int x = 0, i = 0; x < width; x++)
			for(int y = 0; y < height; y++, i++)
				result[i] = ((double) matrix.getValue(x, y)) / 255.0;

		return result;
	}

	/**
	 * Register an Input Node for this Network.
	 * 
	 * @param input
	 *            The Input Node to be registered.
	 */
	protected void registerInput(Input input)
	{
		this.inputs.add(input);
	}

	/**
	 * Register a Layer in this Network.
	 * 
	 * @warning Layers MUST be registered in Input-Hidden-Output order.
	 * @param layer
	 *            The Layer to be registered.
	 */
	protected void registerLayer(Layer layer)
	{
		this.layers.add(layer);
	}

	/**
	 * Register an OutputLayer in this Network.
	 * 
	 * Note that this Layer must also be registered using registerLayer.
	 * 
	 * @param layer
	 *            The Layer to be registered.
	 */
	protected void registerOutputLayer(Layer layer)
	{
		this.outputLayer = layer;
	}
}